move和forward是cpp11引入的两个模板函数, move配合移动语义对应的函数(移动构造, 移动赋值)可以减少不必要的拷贝, 而forward可以完美的保留参数的特性, 从而实现预期的行为.
引用 从cpp11以来, 引用可以分为左值引用、右值引用和通用引用. 左值引用和右值引用分别可以绑定到左值和右值上, 从而在函数调用过程中减少不必要的拷贝. 通用引用用于泛型编程中, 其可以绑定到左值上, 也可以绑定到右值上.
引用折叠 cpp规定不存在指向引用的引用. 在泛型编程中, 如果模板函数指定参数类型为T&& param, 而传入的参数可以是左值也可以是右值, 因此T&&作为通用引用就需要利用引用折叠规则进行参数类型的适配.
如果参数param为左值或左值引用, 那么T就被推导为T&, 这样参数类型就变成了T& &¶m, 引用折叠规则规定这种情况下T为左值引用.
如果参数param为右值, 那么T就被推导为T, 这样参数类型就变成了T&& param.
如果参数param为命名右值引用, 那么T被推导为T&, 因为命名右值引用为左值, T &&无法绑定到左值上去, 因此推导为T&.
如果参数param为匿名右值引用, 那么T被推导为T, 因为匿名右值引用为右值.
Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> using namespace std;template <typename T>void testType (T&& param) { if (is_rvalue_reference<decltype (param)>::value) cout<<"param is rvalue reference\n" ; else cout<<"param is lvalue reference\n" ; } int main () { int val = 10 ; int & L = val; int && R = 123 ; testType (val); testType (L); testType (R); testType (static_cast <int &&>(val)); testType (10086 ); return 0 ; }
Result 1 2 3 4 5 param is lvalue reference param is lvalue reference param is lvalue reference param is rvalue reference param is rvalue reference
move move函数无条件的将传入参数转换为右值引用返回. 由于函数返回右值引用为匿名右值引用, 因此其可以配合移动构造函数和移动赋值函数实现移动语义.
move源码 1 2 3 4 5 6 7 8 9 10 11 template <typename _Tp> _GLIBCXX_NODISCARD constexpr typename std::remove_reference<_Tp>::type&& move (_Tp&& __t ) noexcept { return static_cast <typename std::remove_reference<_Tp>::type&&>(__t ); }
move函数的源码很简单. 函数的参数类型为通用引用, 返回值类型为constexpr typename std::remove_reference<_Tp>::type&&, 其中std::remove_reference<_Tp>::type是偏特化模板, 类似于traits, 提取的是_Tp的去掉引用后的类型(如int&为int, char&&为char). 函数的主体为一条类型转化的语句: static_cast<typename std::remove_reference<_Tp>::type&&>(__t). 其意很明确: 将参数转换为对应类型的右值引用并返回.
使用样例 move函数的过程很简单, 就是简单的类型转换. 其可以认为是一个小的语法糖, 主要配合移动构造函数和移动赋值函数实现移动语义.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <iostream> #include <string> using namespace std;class Array {public : Array () : _size(0 ), _ptr(nullptr ) {} Array (int len) { _size = len; _ptr = new int [_size]; } Array (Array& a) { _size = a._size; _ptr = new int [_size]; for (int i = 0 ; i < _size; i ++ ) _ptr[i] = a._ptr[i]; cout << "called copy-cotr. " << endl; } Array& operator = (Array& a) { if (this == &a) return *this ; if (_ptr != nullptr ) delete _ptr; _size = a._size; _ptr = new int [_size]; for (int i = 0 ; i < _size; i ++ ) _ptr[i] = a._ptr[i]; cout << "called copy operator = " << endl; return *this ; } Array (Array&& a) { _size = a._size; _ptr = a._ptr; a._size = 0 ; a._ptr = nullptr ; cout << "called move cotr." << endl; } Array& operator = (Array&& a) { if (this == &a) return *this ; if (_ptr != nullptr ) delete _ptr; _size = a._size; _ptr = a._ptr; a._size = 0 ; a._ptr = nullptr ; cout << "called move operator = " << endl; return *this ; } ~ Array () { delete _ptr; cout << "called de-cotr." << endl; } void printInfo () { cout << "size of array is " << _size << endl; } private : int _size; int * _ptr; }; int main () { Array arr (10 ) ; Array dst; dst = move (arr); dst.printInfo (); arr.printInfo (); return 0 ; }
总结
move函数无法移动任何东西, 其只是无条件的返回参数的匿名右值引用.
move需要配合移动构造函数和移动赋值函数才能正确实现移动语义.
forward 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 template <typename _Tp>_GLIBCXX_NODISCARD constexpr _Tp&&forward (typename std::remove_reference<_Tp>::type& __t ) noexcept { return static_cast <_Tp&&>(__t ); }template <typename _Tp>_GLIBCXX_NODISCARD constexpr _Tp&&forward (typename std::remove_reference<_Tp>::type&& __t ) noexcept { static_assert (!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp must not be an lvalue reference type" ); return static_cast <_Tp&&>(__t ); }
forward可以有区别转发左值和右值, 其主要特点是通过函数重载实现不同参数类型的分别处理. 如果__t是左值那么进入第一个版本, 如果__t是右值那么进入第二个版本. 通过源码可以发现, 只有_Tp为左值引用时, 参数__t才会被转发成_Tp的左值引用. 否则就是_Tp类型的右值引用.
那么为什么需要forward函数呢? 如果我们希望根据参数引用的类型实现不同的功能, 或者在函数调用中保留原始参数的引用特性, 就必须使用forward函数进行完美转发.
使用样例 Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 #include <iostream> #include <string> using namespace std;class Array {public : Array () : _size(0 ), _ptr(nullptr ) {} Array (int len) { _size = len; _ptr = new int [_size]; } Array (Array& a) { _size = a._size; _ptr = new int [_size]; for (int i = 0 ; i < _size; i ++ ) _ptr[i] = a._ptr[i]; cout << "called copy-cotr. " << endl; } Array& operator = (Array& a) { if (this == &a) return *this ; if (_ptr != nullptr ) delete _ptr; _size = a._size; _ptr = new int [_size]; for (int i = 0 ; i < _size; i ++ ) _ptr[i] = a._ptr[i]; cout << "called copy operator = " << endl; return *this ; } Array (Array&& a) { _size = a._size; _ptr = a._ptr; a._size = 0 ; a._ptr = nullptr ; cout << "called move cotr." << endl; } Array& operator = (Array&& a) { if (this == &a) return *this ; if (_ptr != nullptr ) delete _ptr; _size = a._size; _ptr = a._ptr; a._size = 0 ; a._ptr = nullptr ; cout << "called move operator = " << endl; return *this ; } ~ Array () { delete _ptr; cout << "called de-cotr." << endl; } void printInfo () { cout << "size of array is " << _size << endl; } private : int _size; int * _ptr; }; template <typename T> T wrap (T&& src) { return forward<T&&>(src); } void funcB (Array&& src) { } template <typename T> void funcA (T&& src) { funcB (forward<T&&>(src)); } int main () { Array arr (10 ) ; Array dst1 = wrap (arr); Array dst2 = wrap (move (arr)); dst1.printInfo (); dst2.printInfo (); funcA (move (dst2)); return 0 ; }
Result 1 2 3 4 5 6 7 called copy-cotr. called move cotr. size of array is 10 size of array is 10 called de-cotr. called de-cotr. called de-cotr.
我们在main函数里调用完美转发包装的wrap函数. 通过传入参数的不同实现调用不同的构造函数.
在函数调用中, 由于命名右值引用是左值, 因此其无法进行期望的函数调用, 需要使用forward转发
forward完美转发会保留参数的const和引用特性, 降低出错的可能.
总结 由于命名右值引用是左值, 因此在函数中继续转发该参数会发生不符合预期的结果出现(调用左值引用版本的代码、编译出错等等), 而forward可以完美转发参数的const特性和引用特性, 从而保证执行符合我们的预期, 降低出错的可能性.